﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using BlobNet.Base.Enums;
using BlobNet.Base.Interfaces;
using BlobNet.Base.Storage;

namespace BlobNet.Base
{
    //TODO:quit comment [DebuggerStepThrough]
    [Serializable]
    public abstract class BlobPart : IPersisteable, IDeserializationCallback,ILateInitializable
    {
        #region Attributes

        //TODO: cache dynamic method...reflection.Emit
        //private static readonly object _cachedConstructorsLocker = new object();
        //[NonSerializedAttribute] private static Dictionary<string, ConstructorInfo> _cachedConstructors;
        //private static readonly object _typesCacheLocker = new object();
        //[NonSerializedAttribute] private static Dictionary<string, Type> _typesCache;

        private readonly int _propertyMaxLenght = int.Parse(ConfigurationManager.AppSettings["BlobPropertyNamesMaxLength"]); //TODO: move to configuration object and pass by Injection

        private StatusStorageType _currentStorageType = StatusStorageType.None;
        private IStorageManager _currentStorage;
        private Guid _entityId = Guid.Empty;
        private BlobDomain _blobDomain;
        private IStorageManagerFactory _statusStorageFactory;

        #endregion Attributes

        #region Constructors

        protected BlobPart()
        {
            //TODO: remove this. This is here to be able to use the initialization (ILateInitializable) mechanism by inheritors. 
            //This method of initializing should be changed as is bad exposing this constructor
        }

        protected BlobPart(Guid entityId)
            : this(null, entityId, new StorageFactory())
        {
        }

        protected BlobPart(BlobDomain blobDomain, Guid entityId)
            : this(blobDomain, entityId, new StorageFactory())
        {
        }

        protected BlobPart(BlobDomain blobDomain, Guid entityId, IStorageManagerFactory statusStorageFactory)
        {
            bool callerIsBlobDomainHolder = typeof (BlobDomain).IsAssignableFrom(GetType());

            if (statusStorageFactory == null) throw new ArgumentNullException("statusStorageFactory");
            if (entityId == Guid.Empty)
                throw new ArgumentException("entityId must be set to a non empty value");


            _propertyMaxLenght = int.Parse(ConfigurationManager.AppSettings["BlobPropertyNamesMaxLength"]);

            _blobDomain = blobDomain;
            _entityId = entityId;
            _statusStorageFactory = statusStorageFactory;
            StatusStorage = StatusStorageType.Memory;
            if (!callerIsBlobDomainHolder)
            {
                if (_blobDomain != null)
                    Attach(blobDomain);
            }
            else
            {
                Attach((BlobDomain) this);
            }
        }

        #endregion

        #region Public Properties

        public Guid EntityId
        {
            get { return _entityId; }
        }

        public virtual StatusStorageType StatusStorage
        {
            get { return _currentStorageType; }
            set { //TODO:change this. remove the set and leave the method. Only the Domain can be set from outside the assembly
                ChangeStorage(value);
            }
        }

        protected void ChangeStorage(StatusStorageType value)
        {
            if (value == StatusStorageType.None && _currentStorageType == StatusStorageType.None)
                throw new InvalidOperationException(
                    "CurrentHolder cannot be set with the value BlobPartType.None. This is an initialization and invalidation value only.");

            if (_currentStorageType == StatusStorageType.Persistent && value == StatusStorageType.Memory)
                throw new NotSupportedException("The status holder cannot change from database to memory");

            if (value != _currentStorageType)
            {
                RefreshStorage(value);
                _currentStorageType = value;
            }
        }

        #endregion Public Properties

        #region Non Public Properties

        internal IStorageManager CurrentStorage
        {
            get { return _currentStorage; }
            private set { _currentStorage = value; }
        }

        protected virtual IStorageManagerFactory StatusStorageFactory
        {
            get { return _statusStorageFactory; }
        }

        public bool CanStoreInDataProvider
        {
            get { return Domain != null; }
        }

        public virtual BlobDomain Domain
        {
            get { return _blobDomain; }
            protected set { _blobDomain = value; }
        }

        #endregion Non Public Properties

        #region Not Public Methods

        public virtual void Attach(BlobDomain blobDomain)
        {
            if (blobDomain == null)
                throw new ArgumentNullException("blobDomain");
            if (EntityId == Guid.Empty)
                throw new InvalidOperationException("The identifier of the entity cannot be Guid.Empty");

            Dettach();

            Domain = blobDomain;

            StatusStorage = blobDomain.StatusStorage;
        }
       

        public void Dettach()
        {
            /******************************************************************
             * TODO: avoid the usage of CanDettach and make it always dettachable: 
             * 1.Remove relations from others to this setting them to null
             * 2.The relations from this to others when they are not collections remain orphans(may be not, just diont check this)
             * 3.The relations from this to others when they're collections and the coollection has not more references--> only the collection is removed not its entities 
            *******************************************************************/
            if (!CurrentStorage.CanDettach())
                throw new InvalidOperationException(
                    "The object cannot be dettached from its storage as is in use. Remove first References from collections or from BlobPart variables");

            Domain = null;
            StatusStorage = StatusStorageType.Memory;
        }

        private void RefreshStorage(StatusStorageType holderType)
        {
            if (holderType == StatusStorageType.Persistent)
                //TODO: delete this as entity has always value (BlobDomain == null|| EntityId == Guid.Empty))
            {
                if (Domain == null)
                    throw new InvalidOperationException(
                        "The object must be attached to a blobDomain prior the execution of this operation. Use AttachToBlobPartBlobDomain");

                if (!Domain.CanKeepStatusInDb)
                    throw new InvalidOperationException(
                        "The object is attached to one blobDomain which doesn't allow to keep the status in a database");
            }

            IStorageManager newStorage = StatusStorageFactory.CreateStorage(holderType, EntityId, Domain);
            if (CurrentStorage != null)
            {
                IList<IPersisteable> statusHolders = CurrentStorage.GetVariantSourcesItems();
                foreach (IPersisteable statusHolder in statusHolders)
                {
                    statusHolder.Attach(Domain);
                    statusHolder.StatusStorage = holderType;
                }

                //refresh current storage
                newStorage.FillFrom(CurrentStorage);
                CurrentStorage.ClearData();
            }
            CurrentStorage = newStorage;
        }

        protected internal void Load(StatusStorageType statusType, BlobDomain blobDomain)
        {
            switch (statusType)
            {
                case StatusStorageType.Persistent:
                    //TODO: remove EntityId = entityId;
                    Domain = blobDomain;
                    StatusStorage = StatusStorageType.Persistent;
                    break;

                default:
                    throw new ArgumentOutOfRangeException("statusType");
            }
        }

        protected void SetStatusValue<TPropertyType>(string propertyName, TPropertyType value)
        {
            if (string.IsNullOrEmpty(propertyName))
                throw new ArgumentException("propertyName");

            if (propertyName.Length > _propertyMaxLenght)
                throw new ArgumentException(string.Format("The propertyName must not exceed {0} chars lenght.",
                                                          _propertyMaxLenght));

            CheckPropertyNameCharacters(propertyName);

            CurrentStorage.SetStatusValue(propertyName, value);
        }

        private void CheckPropertyNameCharacters(string propertyName)
        {
            string correctName = Regex.Replace(propertyName, @"[^\w]", string.Empty);
            if (correctName != propertyName)
                throw new ArgumentException(string.Format("the propertyName:{0}, has wrong characters", propertyName));
        }

        protected TPropertyType GetStatusValue<TPropertyType>(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
                throw new ArgumentException("propertyName");

            CheckPropertyNameCharacters(propertyName);

            return CurrentStorage != null
                       ? CurrentStorage.GetStatusValue<TPropertyType>(propertyName)
                       : default(TPropertyType);
        }

        protected bool HasStatusValue(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
                throw new ArgumentException("propertyName");

            CheckPropertyNameCharacters(propertyName);

            return CurrentStorage.HasStatusValue(propertyName);
        }

        //private static BlobPart GetShObject(Type type, BlobDomain blobDomain)
        //{
        //    //TODO: use Dynamic methods and reflection emit
        //    string key = type.ToString();
        //    if (!_cachedConstructors.ContainsKey(key))
        //    {
        //        lock (_cachedConstructorsLocker)
        //        {
        //            if (!_cachedConstructors.ContainsKey(key))
        //            {
        //                //TODO: If we keep reflection as the object creation the empty constructor must exist in the inheritors
        //                var types = new[] {typeof (BlobDomain)};
        //                ConstructorInfo constructorInfo = type.GetConstructor(types) ?? type.GetConstructor(
        //                    BindingFlags.NonPublic | BindingFlags.Instance,
        //                    null, types, null); //TODO: Not Tested 
        //                if (constructorInfo == null)
        //                    throw new InvalidOperationException(string.Format(
        //                        "The type {0} must have an empty constructor.",
        //                        type.FullName));
        //                _cachedConstructors.Add(key, constructorInfo);
        //            }
        //        }
        //    }
        //    return (BlobPart) _cachedConstructors[key].Invoke(new object[] {blobDomain});
        //}

        #endregion Not Public Methods

        #region Public Methods

        //todo make internal or hide it for the inheritors
        public virtual void InitializeObjectStatus(params object[] parameters)
        {
            BlobDomain domain = (BlobDomain) parameters[0];
            Guid entityId = (Guid) parameters[1];
            _blobDomain = domain;
            _entityId = entityId;
            _statusStorageFactory=new StorageFactory();
            _currentStorageType = StatusStorageType.Persistent;

            RefreshStorage(StatusStorageType.Persistent);

            RestoringStatusfromPersistance();
        }

        /// <summary>
        /// Point to implement the creation of the objects that are not stored
        /// </summary>
        protected virtual void RestoringStatusfromPersistance(){}

        //public static BlobPart From(BlobDomain blobDomain, Guid entityId, Type type)
        //{
        //    if (type == null) throw new ArgumentNullException("type");
        //    if (!typeof (BlobPart).IsAssignableFrom(type))
        //        throw new InvalidOperationException(string.Format("The type {0} must be of type BlobPart",
        //                                                          type.FullName));

        //    BlobPart result = GetShObject(type, blobDomain);

        //    result._blobDomain = blobDomain;
        //    result._entityId = entityId;
        //    result._statusStorageFactory = new StorageFactory();
        //    result._currentStorageType = StatusStorageType.Persistent;

        //    result.RefreshStorage(StatusStorageType.Persistent);

        //    return result;
        //}

        //public static BlobPart From(BlobDomain blobDomain, Guid entityId, string fullyQualifiedType)
        //{
        //    if (blobDomain == null) throw new ArgumentNullException("blobDomain");
        //    if (!_typesCache.ContainsKey(fullyQualifiedType))
        //    {
        //        lock (_typesCacheLocker)
        //        {
        //            if (!_typesCache.ContainsKey(fullyQualifiedType))
        //            {
        //                //TODO: Reflection Emit can be applicable here? 
        //                Type type = (from a in AppDomain.CurrentDomain.GetAssemblies()
        //                             from t in a.GetTypes()
        //                             select t).SingleOrDefault(x => x.FullName == fullyQualifiedType);

        //                if (type == null)
        //                    throw new InvalidOperationException(
        //                        string.Format("The type {0} was not found in any Assembly in the AppDomain.",
        //                                      fullyQualifiedType));
        //                _typesCache.Add(fullyQualifiedType, type);
        //            }
        //        }
        //    }
        //    return From(blobDomain, entityId, _typesCache[fullyQualifiedType]);
        //}

        #endregion Public Methods

        #region Implementation of IDeserializationCallback

        public virtual void OnDeserialization(object sender)
        {
            //_cachedConstructors = new Dictionary<string, ConstructorInfo>();
            //_typesCache = new Dictionary<string, Type>();
        }

        #endregion

    
    }
}